/**
 * @file WebGL 相关工具方法
 */
export type Shader = WebGLShader | null
export type Buffer = WebGLBuffer | null
export type Texture = WebGLTexture | null
export type Program = WebGLProgram | null
export type ShaderProgram = WebGLProgram | null
export type UniformLocation = WebGLUniformLocation | null
/**
 * 创建指定类型的着色器，上传 source 源码并编译
 * loadShader 函数将 WebGL 上下文，着色器类型和源码作为参数输入，然后按如下步骤创建和编译着色器：

调用gl.createShader().创建一个新的着色器。

调用gl.shaderSource().将源代码发送到着色器。

一旦着色器获取到源代码，就使用gl.compileShader().进行编译。

为了检查是否成功编译了着色器，将检查着色器参数 gl.COMPILE_STATUS 状态。通过调用gl.getShaderParameter()获得它的值，并指定着色器和我们想要检查的参数的名字（gl.COMPILE_STATUS）。如果返回错误，则着色器无法编译，因此通过gl.getShaderInfoLog()从编译器中获取日志信息并 alert，然后删除着色器返回 null，表明加载着色器失败。

如果着色器被加载并成功编译，则返回编译的着色器。
 * @param { WebGLRenderingContext } gl
 * @param { number } type
 * @param { String } source
 * @returns { WebGLShader | null }
 */
export const loadShader = (
  gl: WebGLRenderingContext,
  type: number,
  source: string): Shader => {
  try {
    const shader = gl.createShader(type)
    if (!shader) {
      console.error('An error occurred creating the shader.')
      return null
    }
    // Send the source to the shader object
    gl.shaderSource(shader, source)
    // Compile the shader program
    gl.compileShader(shader)
    // See if it compiled successfully
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      console.error(
        "An error occurred compiling the shaders: " + gl.getShaderInfoLog(shader),
      )
      gl.deleteShader(shader)
      return null
    }

    return shader
  } catch (e) {
    console.error(e)
    return null
  }
}

/**
 * 初始化着色器程序，让 WebGL 知道如何绘制我们的数据
 * @param { WebGLRenderingContext } gl
 * @param { String } vsSource
 * @param { String } fsSource
 */
export const initShaderProgram = (
  gl: WebGLRenderingContext,
  vsSource: String,
  fsSource: String): ShaderProgram => {
  try {
    const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource as string)
    const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource as string)
    if (!vertexShader || !fragmentShader) {
      return null
    }
    // 创建着色器程序
    const shaderProgram = gl.createProgram()
    if (!shaderProgram) {
      console.error('An error occurred creating the shader program.')
      return null
    }
    // Attach a vertex shader
    gl.attachShader(shaderProgram, vertexShader)
    gl.attachShader(shaderProgram, fragmentShader)
    // Link the program
    gl.linkProgram(shaderProgram)
    // Create a program object
    if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
      console.error(
        "Unable to initialize the shader program: " +
        gl.getProgramInfoLog(shaderProgram),
      )
      return null
    }
    return shaderProgram
  } catch (e) {
    console.error(e)
    return null
  }
}
/**
 * 给顶点着色
在 WebGL 中，物体是由一系列顶点组成的，每一个顶点都有位置和颜色信息。在默认情况下，所有像素的颜色（以及它所有的属性，包括位置）都由线性插值计算得来，自动形成平滑的渐变。我们以前的顶点着色器没有给顶点添加任何特定的颜色——在顶点着色器与片段着色器之间给每个像素着白色，于是整个正方形被渲染成纯白。

现在我们假设正方形的每个顶点使用不同的颜色：红，黄，绿，白，以此渲染一个渐变的色彩。第一步，要给这些顶点建立相应的颜色。首先我们要创建一个顶点颜色数组，然后将它们存在 WebGL 的缓冲区中。为实现这一功能，我们在 initBuffers() 函数中加入如下代码：

这段代码首先建立了一个 JavaScript 的数组，此数组中包含四组四值向量，每一组向量代表一个顶点的颜色。然后，创建一个 WebGL 缓冲区用来存储这些颜色——将数组中的值转换成 WebGL 所规定的浮点型后，存储在该缓冲区中。
 * @param { WebGLRenderingContext } gl
 * @returns { WebGLBuffer | null }
 */
export const initColorBuffer = (gl: WebGLRenderingContext): Buffer => {
  const faceColors = [
    [1.0, 1.0, 1.0, 1.0], // Front face: white
    [1.0, 0.0, 0.0, 1.0], // Back face: red
    [0.0, 1.0, 0.0, 1.0], // Top face: green
    [0.0, 0.0, 1.0, 1.0], // Bottom face: blue
    [1.0, 1.0, 0.0, 1.0], // Right face: yellow
    [1.0, 0.0, 1.0, 1.0], // Left face: purple
  ];
  let colors: number[] = []
  // Convert the array of colors into a table for all the vertices.
  for (let j = 0; j < faceColors.length; ++j) {
    const c = faceColors[j];
    // Repeat each color four times for the four vertices of the face
    colors = colors.concat(c, c, c, c);
  }
  const colorBuffer = gl.createBuffer()
  gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer)
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW)
  return colorBuffer
}
/**
 * 初始化一个缓冲区对象
 * @param { WebGLRenderingContext } gl
 * @param { Float32Array } data
 * @returns { WebGLBuffer | null }
 */
export const initBuffers = (gl: WebGLRenderingContext): Buffer => {
  try {
    const positionBuffer = initPositionBuffer(gl)
    // const colorBuffer = initColorBuffer(gl)
    const textureCoordBuffer = initTextureBuffer(gl);
    const indexBuffer = initIndexBuffer(gl)
    return {
      position: positionBuffer,
      // color: colorBuffer,
      textureCoord: textureCoordBuffer,
      indices: indexBuffer,
    }
  } catch (e) {
    console.error(e)
    return null
  }
}

/**
 * 创建元素（三角形）数组了
 * @param { WebGLRenderingContext } gl
 * @returns { WebGLBuffer | null }
 */
export const initIndexBuffer = (
  gl: WebGLRenderingContext
): Buffer => {
  const indexBuffer = gl.createBuffer()
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
  // This array defines each face as two triangles, using the
  // indices into the vertex array to specify each triangle's
  // position.
  // indices 数组声明每一个面都使用两个三角形来渲染。通过立方体顶点数组的索引指定每个三角形的顶点。那么这个立方体就是由 12 个三角形组成的了。
  // 每个三角形的顶点索引顺序是逆时针的，这样可以保证三角形的正面是向外的。
  const indices = [
    0, 1, 2, 0, 2, 3, // front
    4, 5, 6, 4, 6, 7, // back
    8, 9, 10, 8, 10, 11, // top
    12, 13, 14, 12, 14, 15, // bottom
    16, 17, 18, 16, 18, 19, // right
    20, 21, 22, 20, 22, 23, // left
  ]
  // Now send the element array to GL
  gl.bufferData(
    gl.ELEMENT_ARRAY_BUFFER,
    new Uint16Array(indices),
    gl.STATIC_DRAW,
  )
  return indexBuffer
}
/**
 * 初始化一个位置缓冲区对象
 * 这段代码简单给出了绘画场景的本质。首先，它调用 gl 的成员函数 createBuffer() 得到了缓冲对象并存储在顶点缓冲器。然后调用 bindBuffer() 函数绑定上下文。
 * 当上一步完成，我们创建一个 Javascript 数组去记录每一个正方体的每一个顶点。然后将其转化为 WebGL 浮点型类型的数组，并将其传到 gl 对象的 bufferData() 方法来建立对象的顶点。
 * @param { WebGLRenderingContext } gl
 * @returns { WebGLBuffer | null }
 */
export const initPositionBuffer = (gl: WebGLRenderingContext): Buffer => {
  try {
    // Create a buffer for the square's positions.
    const positionBuffer = gl.createBuffer()
    // Select the positionBuffer as the one to apply buffer
    // operations to from here out.
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
    // Now create an array of positions for the square.
    // 逆时针坐标点(1.0, 1.0) (-1.0, 1.0) (1.0, -1.0) (-1.0, -1.0)
    const positions = [
      // Front face
      -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0,

      // Back face
      -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0,

      // Top face
      -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0,

      // Bottom face
      -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0,

      // Right face
      1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0,

      // Left face
      -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0,
    ]
    // Now pass the list of positions into WebGL to build the
    // shape. We do this by creating a Float32Array from the
    // JavaScript array, then use it to fill the current buffer.
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW)

    return positionBuffer
  } catch (e) {
    console.error(e)
    return null
  }
}
/**
 * 映射纹理到面
 * 我们需要建立纹理坐标到立方体上的面的顶点的映射。这将取代 initBuffers() 中为设置每个立方体面颜色而存在的所有先前的代码。
 * 创建了一个 WebGL 缓冲区，我们将在其中存储每个面的纹理坐标，然后将该缓冲区绑定为将要写入的数组。

textureCoordinates 数组定义好了与每个面上的每个顶点一一对应的纹理坐标。请注意，纹理坐标的取值范围只能从 0.0 到 1.0，所以不论纹理贴图的实际大小是多少，为了实现纹理映射，我们要使用的纹理坐标只能规范化到 0.0 到 1.0 的范围内。

一旦我们建立了纹理映射数组，我们将数组传递到缓冲区中，这样 WebGL 就可以使用该数据了。

然后我们返回新的缓冲区。
 * @param { WebGLRenderingContext } gl
 */
export const initTextureBuffer = (gl: WebGLRenderingContext) => {
  const textureCoordBuffer = gl.createBuffer()
  gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer)
  const textureCoordinates = [
    // Front
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
    // Back
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
    // Top
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
    // Bottom
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
    // Right
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
    // Left
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
  ];

  gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array(textureCoordinates),
    gl.STATIC_DRAW,
  );

  return textureCoordBuffer;

}

// 告诉 WebGL 如何从缓冲区中提取位置数据
export const setPositionAttribute = (gl: WebGLRenderingContext, buffers: any, programInfo: any) => {
  const numComponents = 3; // pull out 2 values per iteration
  const type = gl.FLOAT; // the data in the buffer is 32bit floats
  const normalize = false; // don't normalize
  const stride = 0; // how many bytes to get from one set of values to the next
  // 0 = use type and numComponents above
  const offset = 0; // how many bytes inside the buffer to start from
  gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
  gl.vertexAttribPointer(
    programInfo.attribLocations.vertexPosition,
    numComponents,
    type,
    normalize,
    stride,
    offset,
  );
  gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);
}
// Tell WebGL how to pull out the colors from the color buffer
// into the vertexColor attribute.
export const setColorAttribute = (gl: WebGLRenderingContext, buffers: any, programInfo: any) => {
  const numComponents = 4;
  const type = gl.FLOAT;
  const normalize = false;
  const stride = 0;
  const offset = 0;
  gl.bindBuffer(gl.ARRAY_BUFFER, buffers.color);
  gl.vertexAttribPointer(
    programInfo.attribLocations.vertexColor,
    numComponents,
    type,
    normalize,
    stride,
    offset,
  );
  gl.enableVertexAttribArray(programInfo.attribLocations.vertexColor);
}
// 告诉 WebGL 如何从缓冲区中提取纹理坐标
export const setTextureAttribute = (gl: WebGLRenderingContext, buffers: any, programInfo: any) => {
  const num = 2; // 每个坐标由 2 个值组成
  const type = gl.FLOAT; // 缓冲区中的数据为 32 位浮点数
  const normalize = false; // 不做标准化处理
  const stride = 0; // 从一个坐标到下一个坐标要获取多少字节
  const offset = 0; // 从缓冲区内的第几个字节开始获取数据
  gl.bindBuffer(gl.ARRAY_BUFFER, buffers.textureCoord);
  gl.vertexAttribPointer(
    programInfo.attribLocations.textureCoord,
    num,
    type,
    normalize,
    stride,
    offset,
  );
  gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord);
}

/**
 * 检查是否是 2 的幂
 * @param { number } value
 * @returns { boolean } 
 */
export const isPowerOf2 = (value: number) => {
  return (value & (value - 1)) === 0
}
/**
 * 加载纹理
 * 我们只使用一张单一的纹理贴到立方体的 6 个面上，但是同样的方法可以用来加载任意数量的纹理贴图。
 * 我们将使用 gl.createTexture() 来创建一个纹理对象，然后使用 gl.bindTexture() 来绑定它。
 * 函数 loadTexture() 首先调用 WebGL 的 createTexture() 函数来创建一个 WebGL 纹理对象 texture。接下来使用 texImage2D() 以上传一个蓝色的像素点。这样我们就可以在图片下载完成之前使用这个蓝色的纹理了。

要从图片文件加载纹理，接下来创建一个 Image 对象，并为 src 设置我们想要用作纹理的图片的 URL。我们为 image.onload 设置的函数会在图片下载完成时被调用。那时我们再次调用 texImage2D()，这次我们将图片作为纹理的数据源。之后，我们根据下载的图像在两个维度上是否为 2 的幂来设置纹理的过滤（filter）和平铺（wrap）。

WebGL1 中，对于非 2 的幂纹理只能使用 NEAREST 和 LINEAR 过滤，且不会生成贴图。此外，平铺模式也必须设置为 CLAMP_TO_EDGE。另一方面，如果纹理在两个维度上都是 2 的幂，那么 WebGL 就可以使用更高质量的过滤，可以使用贴图，还能够将平铺模式设置为 REPEAT 或 MIRRORED_REPEAT。

使用重复纹理寻址的一个例子就是使用一张砖块的纹理来平铺满一面墙壁。

多级渐进纹理和纹理坐标重复可以通过调用 texParameteri() 来禁用，当然首先你已经通过调用 bindTexture() 绑定过纹理了。这样虽然已经可以使用非 2 的幂纹理了，但是你将无法使用多级渐进纹理，纹理坐标包装，纹理坐标重复，而且无法控制设备如何处理你的纹理。
浏览器会从加载的图像中按从左上角开始的自上而下顺序复制像素，而 WebGL 需要按自下而上的顺序——从左下角开始的像素顺序。（参见为什么我的 WebGL 纹理是颠倒的？以了解详情。）

所以为了防止渲染时图像纹理方向错误，我们还需要调用 pixelStorei() 并将 gl.UNPACK_FLIP_Y_WEBGL 参数设置为 true，以调整像素顺序，使其翻转成 WebGL 需要的自下而上顺序。
 * @param { WebGLRenderingContext } gl
 * @param { String } url
 * @returns { WebGLTexture | null } 
 */
export const loadTexture = (gl: WebGLRenderingContext, url: string): Texture => {
  const texture = gl.createTexture()
  gl.bindTexture(gl.TEXTURE_2D, texture)
  // Because images have to be downloaded over the internet
  // they might take a moment until they are ready.
  // Until then put a single pixel in the texture so we can
  // use it immediately. When the image has finished downloading
  // we'll update the texture with the contents of the image.
  const level = 0
  const internalFormat = gl.RGBA
  const width = 1
  const height = 1
  const border = 0
  const srcFormat = gl.RGBA
  const srcType = gl.UNSIGNED_BYTE
  const pixel = new Uint8Array([0, 0, 255, 255]) // 蓝色的像素点
  gl.texImage2D(
    gl.TEXTURE_2D,
    level,
    internalFormat,
    width,
    height,
    border,
    srcFormat,
    srcType,
    pixel,
  )
  const image = new Image()
  image.onload = () => {
    // Now that the image has loaded make copy it to the texture.
    gl.bindTexture(gl.TEXTURE_2D, texture)
    gl.texImage2D(
      gl.TEXTURE_2D,
      level,
      internalFormat,
      srcFormat,
      srcType,
      image,
    )
    // WebGL1 has different requirements for power of 2 images
    // vs non power of 2 images so check if the image is a
    // power of 2 in both dimensions.
    if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
      // Yes, it's a power of 2. Generate mips.
      gl.generateMipmap(gl.TEXTURE_2D)
    } else {
      // No, it's not a power of 2. Turn off mips and set
      // wrapping to clamp to edge
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
    }
  }
  image.src = url

  return texture
}